/**
* Copyright 2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package io.neba.core.resourcemodels.caching;
import io.neba.api.resourcemodels.ResourceModelCache;
import io.neba.core.util.Key;
import org.apache.commons.lang.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.request.RequestPathInfo;
import org.apache.sling.api.resource.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.apache.commons.lang.StringUtils.isEmpty;
/**
* A request-scoped {@link ResourceModelCache}. Models added to this cache may either be cached for the entire
* request regardless of state changes (selectors, suffixes, extension, querystring...)
* during the request processing, or in a request-state sensitive manner (see {@link #setSafeMode(boolean)}).
*
* @author Olaf Otto
*/
public class RequestScopedResourceModelCache implements ResourceModelCache, Filter {
private final ThreadLocal<Map<Object, Object>> cacheHolder = new ThreadLocal<>();
private final ThreadLocal<SlingHttpServletRequest> requestHolder = new ThreadLocal<>();
private final ThreadLocal<CacheKeyStatistics> staticsHolder = new ThreadLocal<>();
// The logger is not declared final to allow unit testing
private Logger logger = LoggerFactory.getLogger(getClass());
private boolean enabled = true;
private boolean safeMode = false;
private boolean statisticsEnabled = false;
private String restrictStatisticsToUrlContaining;
/**
* Returns the key for a cacheHolder. If the key changes, the cacheHolder will be cleared.
* The key consists of:
* <ul><li>The current resources page</li>
* <li>the selector string</li>
* <li>the extension</li>
* <li>the suffix</li>
* <li>the query string</li>
* </ul>
*/
private static Key toKey(SlingHttpServletRequest request) {
final RequestPathInfo requestPathInfo = request.getRequestPathInfo();
return new Key(StringUtils.substringBefore(requestPathInfo.getResourcePath(), "/jcr:content"),
requestPathInfo.getSelectorString(),
requestPathInfo.getExtension(),
requestPathInfo.getSuffix(),
request.getQueryString());
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public <T> T get(Object key) {
if (!this.enabled) {
return null;
}
T model = null;
Map<Object, T> cache = (Map<Object, T>) this.cacheHolder.get();
if (cache == null) {
this.logger.debug("No cache found, the cache will not be used.");
} else {
Object internalKey = createInternalKey(key);
model = cache.get(internalKey);
if (isStatisticsEnabled()) {
if (model == null) {
this.staticsHolder.get().reportMiss(internalKey);
} else {
this.staticsHolder.get().reportHit(internalKey);
}
}
}
return model;
}
private boolean isStatisticsEnabled() {
if (!this.statisticsEnabled) {
return false;
}
SlingHttpServletRequest request = this.requestHolder.get();
return request != null && (isEmpty(this.restrictStatisticsToUrlContaining) ||
request.getRequestURI().contains(this.restrictStatisticsToUrlContaining));
}
/**
* {@inheritDoc}
*/
@Override
public <T> void put(Resource resource, T model, Object key) {
if (this.enabled) {
Map<Object, Object> cache = this.cacheHolder.get();
if (cache == null) {
this.logger.debug("No cache found, the cache will not be used.");
} else {
Object internalKey = createInternalKey(key);
cache.put(internalKey, model);
if (isStatisticsEnabled()) {
this.staticsHolder.get().reportWrite(internalKey);
}
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!this.enabled) {
chain.doFilter(request, response);
return;
}
if (!(request instanceof SlingHttpServletRequest)) {
throw new IllegalStateException("Expected a " + SlingHttpServletRequest.class.getName() + ", but got: " + request + ".");
}
final SlingHttpServletRequest slingHttpServletRequest = (SlingHttpServletRequest) request;
this.requestHolder.set(slingHttpServletRequest);
this.cacheHolder.set(new HashMap<>(1024));
if (isStatisticsEnabled()) {
this.staticsHolder.set(new CacheKeyStatistics());
}
try {
chain.doFilter(slingHttpServletRequest, response);
if (isStatisticsEnabled()) {
reportStatistics(slingHttpServletRequest);
}
} finally {
this.cacheHolder.remove();
this.requestHolder.remove();
if (isStatisticsEnabled()) {
this.staticsHolder.remove();
}
}
}
/**
* Logs a statistical report after request processing.
*/
private void reportStatistics(SlingHttpServletRequest request) {
List<CacheKeyStatistics.KeyReport> keyReports = this.staticsHolder.get().getKeyReports();
CacheKeyStatistics.ReportSummary reportSummary = this.staticsHolder.get().getReportSummary();
StringBuilder reportBuilder = new StringBuilder(2048);
reportBuilder.append("Request scoped cache report for ")
.append(request.getMethod()).append(" ")
.append(request.getRequestURI())
.append(":\n")
.append("Hits: ").append(reportSummary.getTotalNumberOfHits())
.append(", misses: ").append(reportSummary.getTotalNumberOfMisses())
.append(", writes: ").append(reportSummary.getTotalNumberOfWrites())
.append(", total number of items: ").append(keyReports.size())
.append('\n');
for (CacheKeyStatistics.KeyReport keyReport : keyReports) {
reportBuilder.append(keyReport.toString()).append('\n');
}
this.logger.info(reportBuilder.toString());
}
/**
* {@inheritDoc}
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
/**
* {@inheritDoc}
*/
@Override
public void destroy() {
}
/**
* The externally provided key may be wrapped to add more key elements in order
* to restrict the cached object's scope to a specific component when safe mode is enabled.
*
* @return A request-state sensitive key in {@link #safeMode}, the original key otherwise.
*/
private Object createInternalKey(Object key) {
if (this.safeMode) {
// Create a request-state sensitive key to scope the cached model to a request with specific parameters.
final SlingHttpServletRequest request = this.requestHolder.get();
if (request != null) {
return new Key(key, toKey(request));
}
}
return key;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setEnableStatistics(boolean enabled) {
this.statisticsEnabled = enabled;
}
public void setRestrictStatisticsTo(String urlFragment) {
this.restrictStatisticsToUrlContaining = urlFragment;
}
public void setSafeMode(boolean safeMode) {
this.safeMode = safeMode;
}
}